#include "palette_widget.h"
#include <cassert>
#include <exception>
#include <QPainter>
#include <QMouseEvent>
#include <QFileDialog>
#include <QColorDialog>
#include <QMessageBox>
#include <QMenu>
#include <QAction>
#include <QSettings>
#include <util/gradient_interpolator.h>

namespace widgets {
  Palette::Palette(QWidget *parent)
    : QWidget(parent),
    stops_(3),
    min_(0),
    max_(1),
    p_action_load_palette_(new QAction(tr("Load palette..."), this)),
    p_action_save_palette_(new QAction(tr("Save palette..."), this)),
    p_action_insert_anchor_(new QAction(tr("Insert anchor"), this))
  {
    stops_ << QGradientStop(0, Qt::black) << QGradientStop(0.5, Qt::red) << QGradientStop(1, Qt::white);
    connect(p_action_load_palette_, SIGNAL(triggered()), this, SLOT(on_action_load_palette()));
    connect(p_action_save_palette_, SIGNAL(triggered()), this, SLOT(on_action_save_palette()));
    connect(p_action_insert_anchor_, SIGNAL(triggered()), this, SLOT(on_action_insert_anchor()));
  }

  Palette::~Palette()
  {

  }

  QGradientStops Palette::stops() const
  {
    return stops_;
  }

  qreal Palette::min() const
  {
    return min_;
  }

  qreal Palette::max() const
  {
    return max_;
  }

  void Palette::setStops(const QGradientStops &stops)
  {
    stops_ = stops;
    update();
    emit stopsChanged(stops_);
  }

  void Palette::setMinMax(qreal min, qreal max)
  {
    min_ = min;
    max_ = max;
    update();
  }

  QGradientStops Palette::load_stops(const QString &file_path)
  {
    QGradientStops stops;
    QSettings settings(file_path, QSettings::IniFormat);
    size_t anchor_num = settings.value("anchor_num", 0).toUInt();
    if (!anchor_num)
      throw tr("invalid palette file");
    for (size_t index = 0; index < anchor_num; ++index)
    {
      settings.beginGroup(QString("anchor%1").arg(index));
      QGradientStop stop(settings.value("value").toDouble(), settings.value("color").value<QColor>());
      stops.push_back(stop);
      settings.endGroup();
    }
    return stops;
  }

  void Palette::paintEvent(QPaintEvent *)
  {
    QPainter p(this);
    QFont font(p.font());
    QFontMetrics font_metrics(font);
    label_height_ = font_metrics.height();

    QRect gradient_rect = get_gradient_rect();

    QLinearGradient gradient;
    gradient.setStart(0, gradient_rect.bottom());
    gradient.setFinalStop(0, gradient_rect.top());
    gradient.setStops(stops_);
    QBrush brush(gradient);
    p.setBrush(brush);
    p.setPen(Qt::black);
    p.drawRect(gradient_rect);

    foreach(const QGradientStop &stop, stops_)
    {
      p.setBrush(stop.second);
      QTransform tf(p.transform());
      p.translate(gradient_rect.right(), gradient_rect.bottom() - stop.first * gradient_rect.height());
      QPolygon poly(3);
      poly[0] = QPoint(-anchor_width, -anchor_height / 2);
      poly[1] = QPoint(0, 0);
      poly[2] = QPoint(-anchor_width, anchor_height / 2);
      p.drawPolygon(poly);
      //    p.translate(gradient_rect.width(), 0);
      p.drawLine(0, 0, tick_size, 0);
      p.translate(tick_size + tick_label_space, 0);
      p.drawText(QRect(0, -label_height_ / 2, width(), label_height_), Qt::AlignVCenter, QString("%1").arg(stop.first * (max_ - min_) + min_));
      p.setTransform(tf);
    }
  }

  void Palette::mouseDoubleClickEvent(QMouseEvent *p_mouse_event)
  {
    qreal nearest = -1;
    const int hit = hit_test(p_mouse_event->pos(), &nearest);

    if (hit < 0 && nearest > 0.0 && nearest < 1.0)
      insert_anchor(nearest);
    else
      choose_anchor_color(hit);
  }

  void Palette::mousePressEvent(QMouseEvent *p_mouse_event)
  {
    qreal nearest = -1;
    const int hit = hit_test(p_mouse_event->pos(), &nearest);

    switch (p_mouse_event->button())
    {
    case Qt::LeftButton:
      if (hit >= 1 && (hit < stops_.size() - 1))
      {
        setMouseTracking(true);
        tracking_anchor_ = hit;
      }
      break;
    }
  }

  void Palette::mouseReleaseEvent(QMouseEvent *p_mouse_event)
  {
    switch (p_mouse_event->button())
    {
    case Qt::LeftButton:
      setMouseTracking(false);
      emit stopsChanged(stops_);
      break;
    }
  }

  void Palette::mouseMoveEvent(QMouseEvent *p_mouse_event)
  {
    if (hasMouseTracking())
    {
      qreal near = nearest(p_mouse_event->pos());
      if (near > 0.0 && near < 1.0)
      {
        stops_[tracking_anchor_].first = near;
        update();
      }
    }
  }

  void Palette::contextMenuEvent(QContextMenuEvent *p_context_menu_event)
  {
    QMenu menu(this);
    menu.addAction(p_action_load_palette_);
    menu.addAction(p_action_save_palette_);
    menu.addSeparator();
    qreal near = -1;
    int hit = hit_test(p_context_menu_event->pos(), &near);
    if (hit < 0 && near > 0 && near < 1)
    {
      p_action_insert_anchor_->setProperty("value", near);
      menu.addAction(p_action_insert_anchor_);
    }
    for (size_t i = 0; i < stops_.size(); ++i)
    {
      QRect stop_rect = get_stop_rect(stops_[i]);
      if (stop_rect.contains(p_context_menu_event->pos()))
      {
        if (i > 0 && i < stops_.size() - 1)
        {
          QAction *p_action_remove_anchor = new QAction(tr("Remove anchor"), &menu);
          p_action_remove_anchor->setProperty("anchor", i);
          menu.addAction(p_action_remove_anchor);
          connect(p_action_remove_anchor, SIGNAL(triggered()), this, SLOT(on_action_remove_anchor()));
        }
        QAction *p_action_set_anchor_color = new QAction(tr("Set anchor color..."), &menu);
        p_action_set_anchor_color->setProperty("anchor", i);
        menu.addAction(p_action_set_anchor_color);
        connect(p_action_set_anchor_color, SIGNAL(triggered()), this, SLOT(on_action_set_anchor_color()));
        break;
      }
    }
    menu.exec(p_context_menu_event->globalPos());
  }

  QRect Palette::get_gradient_rect() const
  {
    const int x1 = border;
    const int y1 = qMax(anchor_height, label_height_) / 2 + border;
    return QRect(x1, y1, gradient_width_, height() - 2 * y1);
  }

  QRect Palette::get_stop_rect(const QGradientStop &stop) const
  {
    const qreal stop_value = stop.first;
    QRect gradient_rect = get_gradient_rect();
    QPoint anchor_center(gradient_rect.right(), gradient_rect.bottom() * (1 - stop_value) + gradient_rect.top() * stop_value);
    return QRect(anchor_center.x() - anchor_width, anchor_center.y() - anchor_height / 2, anchor_width, anchor_height);
  }

  qreal Palette::nearest(const QPoint &pos) const
  {
    QRect gradient_rect = get_gradient_rect();
    qreal near = qreal(gradient_rect.bottom() - pos.y()) / gradient_rect.height();
    if (near < 0)
      near = 0;
    if (near >= 1.0)
      near = 1.0;
    return near;
  }

  int Palette::hit_test(const QPoint &pos, qreal *p_nearest) const
  {
    for (int i = 0; i < stops_.size(); ++i)
    {
      const int index = (i + 1) % stops_.size();
      const QGradientStop &stop = stops_[index];
      QRect stop_rect = get_stop_rect(stop);
      if (stop_rect.contains(pos))
      {
        if (p_nearest)
          *p_nearest = stop.first;
        return index;
      }
    }
    if (p_nearest)
      *p_nearest = nearest(pos);
    return -1;
  }

  void Palette::insert_anchor(qreal pos)
  {
    QLinearGradient gradient;
    gradient.setStops(stops_);
    gradient.setColorAt(pos, util::GradientInterpolator(stops_)(pos));
    stops_ = gradient.stops();
    update();
    emit stopsChanged(stops_);
  }

  void Palette::choose_anchor_color(size_t index)
  {
    QColor col = QColorDialog::getColor(stops_[index].second, this);
    if (col.isValid())
    {
      stops_[index].second = col;
      update();
      emit stopsChanged(stops_);
    }
  }

  void Palette::on_action_insert_anchor()
  {
    insert_anchor(sender()->property("value").toDouble());
  }

  void Palette::on_action_remove_anchor()
  {
    size_t index = sender()->property("anchor").toUInt();
    stops_.remove(index);
    update();
    emit stopsChanged(stops_);
  }

  void Palette::on_action_set_anchor_color()
  {
    choose_anchor_color(sender()->property("anchor").toUInt());
  }

  void Palette::on_action_load_palette()
  {
    using namespace std;
    try
    {
      QString file_path = QFileDialog::getOpenFileName(this, tr("Load palette"), QString(), tr("Palette (*.pal)"));
      if (!file_path.isEmpty())
      {
        stops_ = load_stops(file_path);
        update();
        emit stopsChanged(stops_);
      }
    }
    catch (QString &str)
    {
      QMessageBox::critical(this, tr("Palette loading error"), str);
    }
  }

  void Palette::on_action_save_palette()
  {
    QString file_path = QFileDialog::getSaveFileName(this, tr("Load palette"), QString(), tr("Palette (*.pal)"));
    if (!file_path.isEmpty())
    {
      QSettings settings(file_path, QSettings::IniFormat);
      settings.clear();
      settings.setValue("anchor_num", stops_.size());
      for (size_t index = 0; index < stops_.size(); ++index)
      {
        settings.beginGroup(QString("anchor%1").arg(index));
        settings.setValue("value", stops_[index].first);
        settings.setValue("color", stops_[index].second);
        settings.endGroup();
      }
    }
  }

} // namespace widgets {